package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.ChargingDataRepository;
import cn.tedu.charging.cost.dao.repository.ChargingRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
@Slf4j
public class CostServiceImpl implements CostService {
    /**
     * map 用来记录充电次数
     * key 订单号 , value 实际充电的数据(度数,次数,花费)
     * 问题
     * 1 并发安全
     * 2 没有持久化, 重启后数据后丢失,同步的数据丢失,从新开始算
     * 3 数据量问题, 计价服务 部署到一台服务器 4C 8G 不重启 一直运行 运行了一年 1000个订单 * 30s  = 大 ,
     * 设备高频的同步充电进度数据,数据量太大 导致内存溢出
     * 4 分布式场景
     * 数据独享问题 计价服务部署了多台
     * 4040 本地Map 4040访问不了4041
     * 4041 本地Map 4041访问不了4040
     *
     * 需要改成共享
     * ES独立部署,共享的数据,多个计价服务都访问的是同一份数据
     *
     * 存储到 ES  一次全搞定   Redis
     *
     * 技术选型 合适的业务选择合适解决方案
     *
     * 设备会同步大量的充电数据到业务服务(订单服务,价格服务)
     * 数据需要进行持久化, 有多种持久化的数据库
     * mysql redis es
     * MySql 适合放ACID要求严格的数据,Mysql为了实现高ACID,性能有所下降
     * 用户量较少 单机mysql
     * 用户量增加 分库分表
     * 用户量继续增加 继续分库分表
     *
     * Redis 分布式的非关系型内存数据库 基于内存,性能高,内存容量有限,做了持久化,会导致性能下降
     *
     * ES 分布式的搜索引擎,数据的存储
     *
     */


    private Map<String, ChargingData> chargingProcessData = new HashMap<>();

    /**
     * 计算电费
     * 充电度数(设备同步的充电度数) * 计价规则单价(匹配的计价规则)
     * @param chargingProcessParam
     * @return
     */
    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        // 通过本地map保存
//        ChargingData chargingData = getChargingData(chargingProcessParam);   // 把所有都传递过去
        ChargingDataPO chargingDataES = getChargingDataES(chargingProcessParam);
        log.debug("获取充电数据第:{}次充电",chargingDataES);
        log.debug("通过订单号:{},当前同步次数:{},获取充电数据:{}",chargingProcessParam.getOrderNo(),chargingDataES.getCount(),chargingDataES);

        int hour = getHour();
        log.debug("获取当前 小时：{}", hour);
        // 获取场站id
        Integer stationId = chargingProcessParam.getStationId();
        // 获取计价规则：通过场站id和时间(小时)获取匹配的计价规则
        CostRulePO matchCostRuleByGunAndTime = getMatchCostRuleByGunAndTime(stationId, hour, chargingProcessParam.getGunId());// 传参        log.debug("获取当前时间 小时:{},获取计价规则:{}", hour, matchCostRuleByGunAndTime);
        // 以上完成了计价规则匹配

        /**
         * 设备和订单同步的充电度数
         *         次数 充电度数
         *
         *         方案A 每次同步的是 实际重了多少度电  目前
         *          1:    5
         *          2:    4  丢失   2:    4
         *          3:    5
         *         优点:计算简单
         *          缺点:设备需要记录每次充了多少度电
         *          1:5 2:4 记录1:5 已经同步成功了,需要记录实际度数,还需记录状态
         *          5 + 4 + 5 = 14 必须一次数据同步都不能丢 丢了钱就计算错误
         *
         *         胜出 方案B  每次同步的是 已经充了多少度电
         *          1:    5   1.5
         *          2:    9   2
         *          3:    14  1.5
         *          5 * 1.5 + (9-5) * 2  + (14-9) * 1.5
         *
         *  采用 方案B 进行计算
         */
        if (matchCostRuleByGunAndTime != null){
            // 获取计价名称
            String name = matchCostRuleByGunAndTime.getName();
            log.debug("获取计价规则名称:{}",name);
            // 获取当前计价规则的价格 每度电的价格   最后订单完成的时候费用 再 + 服务费
            BigDecimal powerFee = matchCostRuleByGunAndTime.getPowerFee();
            // 判断是否是第一次
            if (isFirst(chargingDataES.getCount())){
                firstCost2ChargingData(chargingProcessParam,chargingDataES,name,powerFee,hour);
            }else {
                lastCost2ChargingData(chargingProcessParam,chargingDataES,name,powerFee,hour);
            }
        }else{
            log.error("场站信息:{},当前时间(小时):{}获取计价规则失败，告警告警告警，接入告警系统",stationId,hour);
        }


        // TODO 计价服务返回价格计算的结果
        log.debug("chargingDataES的参数信息有：{}",chargingDataES);
        return buildChargingProcessVo(chargingDataES);
    }

    /**
     * 封装返回结果 ChargingProcessVO
     */
    private ChargingProcessVO buildChargingProcessVo(ChargingDataPO chargingData){
        log.debug("ChargingData信息有：{}",chargingData);
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        // 手动复制属性，适用于参数较少的情况
//        chargingProcessVO.setOrderNo(chargingData.getOrderNo());
        BeanUtils.copyProperties(chargingData,chargingProcessVO);
        
        // 计算电费单价：总电费 / 充电度数
        if (chargingData.getChargingCapacity() != null && chargingData.getChargingCapacity() > 0) {
            BigDecimal powerFee = chargingData.getTotalCost().divide(BigDecimal.valueOf(chargingData.getChargingCapacity()), 4, BigDecimal.ROUND_HALF_UP);
            chargingProcessVO.setPowerFee(powerFee);
        }
        
        // 计算充电时长：当前时间 - 开始时间
        if (chargingData.getStartTime() != null) {
            long currentTime = System.currentTimeMillis();
            long durationMillis = currentTime - chargingData.getStartTime();
            // 转换为秒
            long durationSeconds = durationMillis / 1000;
            // 格式化为 HH:mm:ss 格式
            long hours = durationSeconds / 3600;
            long minutes = (durationSeconds % 3600) / 60;
            long seconds = durationSeconds % 60;
            String totalTime = String.format("%02d:%02d:%02d", hours, minutes, seconds);
            chargingProcessVO.setTotalTime(totalTime);
        }
        
        return chargingProcessVO;
    }



    /**
     * 通过 es 同步充电数据
     */
    @Autowired
    private ChargingDataRepository chargingDataRepository;

    private ChargingDataPO getChargingDataES(ChargingProcessParam chargingProcessParam){
        //从ES通过订单号获取对应的充电数据
        String orderNo = chargingProcessParam.getOrderNo();
        /**
         * 返回值 Optional<ChargingData>: 返回一个 Optional 对象，其中包含查询结果
         *      如果找到了对应的记录，Optional 中会包含该 ChargingData 对象
         *      如果没有找到对应的记录，Optional 会为空
         * 使用 Optional 的好处是：
         *      避免了返回 null 值可能引起的 NullPointerException
         *      提供了一种更优雅的方式来处理可能为空的情况
         */
        Optional<ChargingDataPO> optional = chargingDataRepository.findById(orderNo);
        if (optional.isPresent()){
            // 如果找到了数据，第N次同步
            ChargingDataPO chargingDataPO = optional.get();   // 直接返回es数据
            chargingDataPO.setCount(chargingDataPO.getCount() + 1);
            return chargingDataPO;
        }else {
            // 如果没找到，第一次同步数据
            ChargingDataPO chargingDataPO = initEsChargingData(chargingProcessParam);
            chargingDataRepository.save(chargingDataPO);
            return chargingDataPO;
        }

    }

    private ChargingDataPO initEsChargingData(ChargingProcessParam chargingProcessParam) {
        ChargingDataPO chargingDataPO = new ChargingDataPO();
        chargingDataPO.setId(chargingProcessParam.getOrderNo());
        chargingDataPO.setOrderNo(chargingProcessParam.getOrderNo());
        chargingDataPO.setGunId(chargingProcessParam.getGunId());
        chargingDataPO.setUserId(chargingProcessParam.getUserId());
        chargingDataPO.setStartTime(System.currentTimeMillis());
        chargingDataPO.setCount(1);
        return chargingDataPO;
    }


    /**
     * 充电数据同步
     * @param chargingProcessParam
     * @return
     * 这个方法是使用本地 map 进行存储的
     */
    private ChargingData getChargingData(ChargingProcessParam chargingProcessParam){
        // 从 map 中获取订单号对应的充电数据
        String orderNo = chargingProcessParam.getOrderNo();
        ChargingData chargingData = chargingProcessData.get(orderNo);
        if (chargingData == null){
            // 第一次同步数据(第一次初始化)
            chargingData = initChargingData(chargingProcessParam);
            // 保存初始化的充电数据到map
            chargingProcessData.put(orderNo,chargingData);
        }else {
            // 不是第一次同步数据
            chargingData.setCount(chargingData.getCount() + 1);
            chargingProcessData.put(orderNo,chargingData);
        }
        return chargingData;
    }

    /**
     * 第一次同步充电状态数据
     * 创建数据同步记录对象 ChargingData
     * 设置
     * 订单信息(orderNo)  设备信息(gunId)  用户信息(userId)
     * 充电信息(充电度数)
     * @param param
     * @return
     */
    private ChargingData initChargingData(ChargingProcessParam param) {
        ChargingData chargingData = new ChargingData();
        chargingData.setOrderNo(param.getOrderNo());
        chargingData.setGunId(param.getGunId());
        chargingData.setUserId(param.getUserId());
        chargingData.setStartTime(System.currentTimeMillis());
        chargingData.setCount(1);
        return chargingData;
    }


    /**
     * 计算第一次的价格
     */
    private void firstCost2ChargingData(ChargingProcessParam chargingProcessParam, ChargingDataPO chargingData,String name, BigDecimal powerFee, Integer hour) {
        log.debug("第一次同步充电数据");
        // 获取充电的度数
        Float chargingCapacity = chargingProcessParam.getChargingCapacity();
        log.debug("获取充电的度数:{}",chargingCapacity);
        // 计算价格
        BigDecimal cost = getCost(chargingCapacity, powerFee);
        log.debug("当前时间(小时):{},"+
                "计价规则名称:{}," +
                "充电度数:{}," +
                "电费单价:{},本次花费:{}",hour, name, chargingCapacity, powerFee, cost);

        // 保存当前的花费到ChargingData中，进行保存,为了下次同步
        chargingData.setTotalCost(cost);
        // 保存当前的充电度数到ChargingData中，为了下次同步
        // 比如：
        // 第一次：同步充电度数为 5 度；第二次：同步充电度数为 15 度；
        // 说明第一次实际充了 5 度；第二次实际充了 15-5 = 10度(因为是包含关系)
        // 同步的充电度数包含上次的充电度数,真实充了多少(15-5)
        // 为了下次同步做<<累加花费>>
        chargingData.setChargingCapacity(chargingCapacity);
        // 不存在就保存，存在通过id进行存储
        chargingDataRepository.save(chargingData);    // 持久化数据：使用 Repository 的 save 方法将修改后的数据写入 Elasticsearch
    }



    /**
     * 判断同步充电是否是第一次
     * @return
     */
    private Boolean isFirst(Integer count){
        return count==1;
    }

    /**
     * 不是第一次 第N次 计算价格
     * @return
     */
    private void lastCost2ChargingData(ChargingProcessParam chargingProcessParam, ChargingDataPO chargingData,String name, BigDecimal powerFee, Integer hour){
        log.debug("不是第一次同步充电数据");
        // 获取本次的充电度数
        Float currentChargingCapacity = chargingProcessParam.getChargingCapacity();
        // 获取上次的充电度数
        Float lastChargingCapacity = chargingData.getChargingCapacity();
        log.debug("本次的充电度数:{},上次的充电度数:{}",currentChargingCapacity,lastChargingCapacity);
        // 真实充电度数
        Float realChargingCapacity = currentChargingCapacity - lastChargingCapacity;
        // 保存本次充电的度数,为下次做准备
        chargingData.setChargingCapacity(currentChargingCapacity);
        // 计算本次费用
        BigDecimal cost = getCost(realChargingCapacity, powerFee);
        // 拿到上次的花费
        BigDecimal lastCost = chargingData.getTotalCost();
        // 当前总花费 = 上次总花费 + 本次花费
        BigDecimal totalCost = lastCost.add(cost);
        // 保存当前总花费
        chargingData.setTotalCost(totalCost);
        log.debug("当前时间(小时):{},"+
                "计价规则名称:{},"+
                "充电度数:{},"+
                "当前度数:{},"+
                "电费单价:{},本次花费:{},总花费:{}",hour,name,realChargingCapacity,currentChargingCapacity,powerFee,cost,totalCost);

        chargingDataRepository.save(chargingData);
    }



    // 获取当前服务器的时间
    private int getHour(){
        LocalDateTime now = LocalDateTime.now();
        return now.getHour();
    }

    /**
     * 计算价格  度数 * 电单价(每度电的价格)
     * @param chargingCapacity
     * @param powerFee
     * @return
     */
    private BigDecimal getCost(Float chargingCapacity, BigDecimal powerFee){
        // 把 Float 类型的 chargingCapacity 转换成 String 
        String stringChargingCapacity = Float.toString(chargingCapacity);
        // 把 String 类型的 stringChargingCapacity 转换成 BigDecimal
        BigDecimal bigDecimal = new BigDecimal(stringChargingCapacity);
        return powerFee.multiply(bigDecimal);    // multiply后面需要传入一个 BigDecimal，需要把chargingCapacity转换成BigDecimal
    }



    /**
     * 通过 时间(设备同步充电状态的时间的小时) 和 枪类型
     * 从 当前站点里所有的计价规则列表 去匹配 计价规则
     * @param hour
     * @param gunId
     * @return
     */
    public CostRulePO getMatchCostRuleByGunAndTime(Integer stationId,Integer hour,Integer gunId){
        log.debug("通过场站{}信息，获取规则列表",stationId);
        List<CostRulePO> costRules = getCostRules(stationId);
        for (CostRulePO costRulePO : costRules) {
            log.debug("开始匹配规则:{}",costRulePO);
            if(costRulePO.getGunType().equals(gunId) && costRulePO.getStartTime() <= hour && costRulePO.getEndTime() >= hour){
                log.debug("匹配成功:{}",costRulePO);
                return costRulePO;
            }
        }
        return null;
    }




    @Autowired
    private ChargingRuleRepository chargingRuleRepository;
    /**
     * 通过场站Id获取计价规则
     * 出参是 List<CostRulePO> 因为返回的是多个值
     */
    private List<CostRulePO> getCostRules(Integer stationId){
        log.debug("通过场站Id:{},获取计价规则",stationId);
        List<CostRulePO> costRulePOS = chargingRuleRepository.getCostRules(stationId);
        log.debug("通过场站Id:{},获取计价规则:{}",stationId,costRulePOS);
        return costRulePOS;
    }

}
